<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>手势识别文档 </title>
</head>


<body>
<b>2019-2-17最后更新</b>

<hr />
<h2>安装 </h2>
<div>
    大部分相关包的文件我都放在package文件夹下了，如果有遗漏或者不匹配的，自己去官网也很容易找到。
    对于Python依赖包，最方便的安装方法是电脑联网之后使用pip 工具自动安装，后面将有相关的介绍。
    本套代码依赖于以下Python包:
    <ol>
    <li>numpy 1.16.1 ----数学计算工具</li>
    <li>scipy 1.2.1 ----科学计算工具</li>
    <li>scikit_learn 0.20.2 ----机器学习算法库</li>
    <li><strike>web.py 0.40 ----服务器框架</strike>（web.py目前似乎不支持Python3.7，我只在Python 3.6.0下测试能用）</li>
    <li>pillow 5.4.1 ----图像处理库</li>
    </ol>
</div>

<h4>Python 开发环境的安装</h4>
<div>
    我在windows上使用的是Python 3.6.0，<strike>理论上用高于这个版本的Python也没有什么问题</strike>，（Python3.7
    似乎会不兼容web.py，CSDN上能找到解决办法，不过不懂原理）。Mac OS应该是自带Python的，
    可以在命令行中输入<b>Python</b>，如果已经安装了Python的话，就会进入Python的交互模式，在响应的开头中可以看到
    Python的版本号。我也把mac下Python 3.6.0的安装包下载好放在package文件夹下了。因为觉得应该用不到，所以就没去
    了解Mac下怎么安装，如果有问题的话，可以看
    <a target="blank" href="https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014316090478912dab2a3a9e8f4ed49d28854b292f85bb000">
    廖雪峰的教程</a>，再问下Bing娘应该不难解决。这里给出Python官网的：
    <a target="blank" href="https://www.python.org/downloads/mac-osx/">下载链接</a>
</div>


<h4>使用pip安装依赖包</h4>
<div>
    安装依赖包最简单暴力的方法是使用pip工具。以安装numpy为例，首先确定电脑联网，然后打开终端（windows下是cmd），输入：
    <blockquote>pip3 install numpy</blockquote>
    就会选择匹配当前电脑系统的最新版本并自动下载安装，安装过程中如果依赖其他包也会自动下载安装。如果下载失败，可以在终端
    中把工作目录切换到package文件夹下（Mac继承自Unix，所以应该也是cd命令），使用本地文件安装，输入:
    <blockquote>pip3 install numpy-1.16.1-cp36-cp36m-win_amd64.whl</blockquote>
    把上一条命令中的numpy改成本地安装包文件全名就可以了
    <br /><img src="pip-install.png" width="600px"/>
    <br /><br />
    <strike>比较特殊的是web.py，直接使用pip命令会下载0.39版本，不兼容Python 3。目前支持Python 3的只有0.40版本的开发版，需要添加参数
    <blockquote>pip3 install web.py==0.40-dev1</blockquote>
    安装。这些在<a href="http://webpy.org/" target="blank">官网首页</a>都有说明。另一种方法是从源代码安装，将我下载的“web.py-0.40.dev1.tar.gz”
    解压，终端下进入解压后的目录（该目录下有个文件setup.py），在该目录运行以下命令即可安装:
    <blockquote>py -3 setup.py install</blockquote>
    </strike>
</div>


<h4>Python 语言的学习</h4>
<div>
    基础语法部分，一些中文的在线教程就已经足够了，我看的比较多的是<a href="https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000">
    廖雪峰的Python教程</a>，讲得非常好。最近网上出现一个“菜鸟教程”的网站，上面很多资源也很不错，但是我看得不多。如果想看一些出版
    读物的话，可以问Bing娘搜索一下书单，然后选出现频次比较多的，豆瓣评分8.0以上的书入门都差不多。最最一手的资料是Python
    官方和第三方库官方的文档，当你看完教程，需要深入了细节的时候，官方文档往往是给出了最多细节的资料。
</div>


<h4>关于Git</h4>
<div>
    写好的代码我会放到gitee公开，这样以后我有更新的话就可以实时提交到gitee。另一方面，交流在开发中的代码，如果
    用QQ传文件，数量多了容易搞乱，用git克隆到自己电脑之后，每次拉取最新的代码只需要一句git pull就可以了，非
    常方便。你也可以fork到自己的账号下，这样以后需要的时候，你就可以在自己账号下找到代码了。关于git的入门，看
    看廖雪峰git教程就足够了，我也没有完全学会上面的东西。
</div>


<br /><hr />
<h2>使用</h2>
<div>
    目前用来识别手写数字的要运行的文件有2个，一个是server2.py，另一个是main2.py。
    <br />
    server2.py用来运行一个服务器，这样在本机的浏览器中打开链接：“http://localhost:port”就会进入一个网页，在这个
    网页里可以书写数字，书写完成之后可以上传字迹。如果手机和电脑在同一个局域网中，手机可以访问http://电脑的IP地址+端口进入这个网页。如果
    局域网网速不好导致网页加载慢的话，可以手机开个热点给电脑连，或者反过来，这样加载起来就很快了。当通过网页上传字迹
    时，server2.py会在同目录下将字迹图片保存为sample.png，将轨迹坐标保存为sample.txt。每上传一次，就覆盖上一次的文件。
    <br/>
    main2.py是主程序，此外还有一个main.py，因识别率不高，已放弃。main2.py监测同目录下sample.png和sample.txt的修改时间，
    发现文件被修改就读取文件，并且识别其中的数字，在终端中打印出来。
</div>

<br /><br /><hr />
<h2>手写数字识别的原理</h2>
<h4>参考资料</h4>
<div>
    我用的也是比较原始的方法，没什么人工智能的技术。在找数据集的时候发现，老美在98年放出数据集的前就已经详细研究过
    这个技术了，这里我主要参考的是以下两个教程：
    <ol>
        <li><a href="https://www.imooc.com/learn/131" target="blank">iMOOC的图形手势识别教程</a></li>
        <li>中国大学MOOC上北理工的《Python机器学习应用》 第5、6单元
            <br/><img src="BIT-MOOC-1.png" width="300px"/></li>
        <li>人教版高中数学 B版 必修4 P137</li>
    </ol>
    <br/>
    其实也没什么很深刻的思想，只是简单推广了一下向量的概念，高中生都能理解，并不难。当年给一个高二的学弟提到了这个
    原理，他就结合加速度计搞了个手势识别玩VR游戏。实现中比较麻烦的应该是算法，不过算法已经封装在Python的
    scikit-learn库中了，调用起来只需传几个参数，所以实际上用Python实现这个任务还是很简单的。原理在上面的
    资料里已经讲得很清楚了，先看下高数必修4的P137，理解了将三维向量推广到n维向量，知道该如何计算n维空间的
    距离，然后看iMOOC的手势识别教程加深理解，就能搞清楚原理了。我采用的是KNN算法，北理工的MOOC的第5单元介
    绍了KNN算法，并且有一个手写识别的实例，用来介绍如何使用KNN算法，我的手写识别的代码主要就是参照这个实例的。
</div>


<h4>向量概念的推广</h4>
<div>
    直接上图：
    <br />
    <img src="MathBook.jpg" width="800px">
</div>


<h4>数据集</h4>
<div>
    识别的关键是数据集。程序将得到的字迹与数据集中数据进行比对，选出最相似的数字作为识别的结果。这里我尝试了两个数据集，
    分别介绍如下。
    <br /><br />
    <b>1.北理工MOOC附带的数据集-北理工数据集</b>
    <br />
    可以在课程公告中找到老师给的百度云地址<a target="blank" href="https://pan.baidu.com/s/1eR7doh8">https://pan.baidu.com/s/1eR7doh8</a>
    ，上面可以下载所需数据集。有位同学整理了该课程的所有资料，在github上共享<a target="blank" href="https://github.com/caiiiac/Machine-Learning-with-Python/tree/master/课程数据/手写数字识别">
    https://github.com/caiiiac/Machine-Learning-with-Python/tree/master/课程数据/手写数字识别</a>
    <br /><br />
    关于这个数据集，课程中已经讲得很透彻了，我就不再赘述。对应于我的代码是knn_recog.py,img_preprocess.py和main.py。
    操作是，得到用户手写字迹的图片后，img_preproces.py负责将数字所在区域抠出来，缩放到高度填满整个图片，然后使用课程中所述方法和
    训练集比对。数据集分训练集和测试集，使用测试集测试时，识别正确率高达97%，但是实际识别我手写的字迹，正确
    率却偏低。推测原因一方面是我没法将图片处理得和训练集尽可能相似，并且数字的宽度可能影响识别结果，这一点不太好处理。
    另一方面我觉得这个数据集还是有点小，一个数字的图片才200个左右。所以就决定尝试下课程中提到的另一个数据集。
    <br /><br />

    <b>2.DBRHD数据集</b>
    北理工MOOC第6单元“手写识别”实例介绍文档中给出了数据集的下载地址：<a target="blank" href="https://archive.ics.uci.edu/ml/datasets/PenBased+Recognition+of+Handwritten+Digits">https://archive.ics.uci.edu/ml/datasets/PenBased+Recognition+of+Handwritten+Digits</a>
    但是我发现MOOC上讲的似乎有些不对。慕课上说DBRHD（Pen-Based Recognition of Handwritten Digits）数据集是图片，
    而且他们采用的就是DBRHD数据集。我发现事情的真相不是这样的。我下载了DBRHD，但是没有找到任何图片，只有6个纯文本
    文件，和老师讲的完全不一样。闲话少叙，发现的曲折就不多说了。为了区分，这里我把MOOC实例中用的数据集称为
    “北理工数据集”，下面这个才是用老师给的网址下载真正的DBRHD。
    <br /><img src="dbrhd-overview.png" /><br />
    <br />下面开始干货。
    <br/>可以看到，DBRHD一共有5个文件，都是纯文本文件，在下载页面可以看到该数据集的英文介绍，pendigits.names中也有一份同样的介绍。
    这里我以我的理解大致转述一下，可以跟pendigits.names中的第4点——Relevant Information对照着看。
    <br /><br />
    研究人员收集了44位书写者的手写样本，其中30人的样本用来做训练、交叉验证、以及与用户关联的验证（writer dependent testing），
    其余14人的样本被用来做用户无关的验证(writer independent testing)。研究人员用手写笔和一个分辨率500*500的平板采集用户输入。
    当用户在书写时，平板每隔100ms采集一次笔的（x，y）坐标。(原文中 $x$, $y$,应该是把字母包围起来表示强调，并无其他意思，以及后文出现的
    （$x,y$）亦然)。采集到的原始数据取值在0到500之间（平板的分辨率），研究人员首先把这些坐标归一化（normalize），将坐标变换
    成取值在0到100之间，以消除数字位置和大小不同带来的影响（原始数据中，数字不一定占满整个500*500的屏幕，这里的操作应该也是把
    数字抠出来，然后缩放到占满整个100*100的空间，即“归一”成同样的大小）。
    <br /><br />

    为了训练分类器，我们需要把字迹用等维度的特征向量（constant length feature vectors）表示。所谓等维度，就是坐标轴的数量
    相等，例如：
    <br /><b>v1</b> = [x1, x2, x3, x4]
    <br /><b>v2</b> = [y1, y2, y3, y4]
    <br /><b>v3</b> = [z1, z2, z3, z4, z5, z6]
    <br/> <b>v1</b>和<b>v2</b>都是4维向量，是等维度的，<b>v3</b>是6维向量，和<b>v1</b>，<b>v2</b>的维度不相等。<b>v1</b>
    和<b>v2</b>可以互相加减：
    <br /><b>v1</b> + <b>v2</b> = [x1+y1, x2+y2, x3+y3, x4+y4];
    <br /><br />计算距离:
    <br />由2维下的公式推广得出,4维空间中<b>v1</b>和<b>v2</b>终点的距离d= sqrt((x1-y1)**2 + (x2-y2)**2 +
    (x3-y3)**2 + (x4-y4)**2)

    <br /><br />可见，<b>v3</b>不能和<b>v1</b>或<b>v2</b>运算。
    所以，为了各个字迹之间能相互计算相似度，就要将字迹转化成固定维数的特征向量，方法就是对原始数据重新采样（resample）。

    <br/><br/>原始数据是一系列(x_t, y_t)的坐标点，这里t表示下标，相当于x(t), y(t)，x，y随时间变化的意思。原始数据
    是等时间间隔（100ms）采样的，由于每个人书写的速度不一样，所以坐标点之间的距离也因人而异。重采样则是将原始数据中
    的坐标点用直线依次连起来形成轨迹，新的采样点将轨迹等长度分成好几段。这就是原文中“在每对坐标点之间做简单
    的线性插值（simple linear interpolation between pairs of points）的等弧长划分（regular spaced in arc length）”。

    <br /> <br/>So, 最终老美做实验的结论是当将原始数据重采样为8个坐标点，也就是2*8=16个坐标值时，识别的准确率和计算强度上
    达到了最好的平衡。数据集中的数据就是已经处理好的16个坐标值的序列。显然，采样点越多，对轨迹的还原程度就越高，识别也就
    应当越准确。老美做这个实验的时候是98年，这么多年过去了，电脑的性能翻了成百上千倍，不再在乎这点计算量了。老美在数据集
    中还给了原始数据，如果后期需要进一步提高准确度，可以直接对原始数据做更精确的重采样。
    <br /><br />
</div>


<h4>终于要亲眼看看数据集了</h4>
<div>
    数据集中pendigis.tra和penditis.tes是老美已经处理好的数据。望文生义，tra是training的缩写，tes是test的缩写，分别表示
    训练集和测试集，前面已经解释过了。这里看下训练集的格式。前16个数值是重采样后的坐标点，取值0到100，表示一个16维的向量，
    最后一个数值是该向量所代表的数字，被称为“标签（label）”。
    <br /><img src="dataset-overview.png" width="800px"><br />
    <br />这里在向量的表示上可能会有点理解上的小问题。数据集中一个向量的形式是这样的：
    <br /><blockquote>0, 89, 27,100, 42, 75, 29, 45, 15, 15, 37,  0, 69,  2,100,  6</blockquote>
    <br />可是从前面一大堆的叙述中，我们觉得形式（一系列二维坐标点）应该是这样的:
    <br /><blockquote>(0, 89), (27,100), (42, 75), (29, 45), (15, 15), (37,  0), (69,  2),(100,  6)</blockquote>
    <br />前者是16维空间中的一个坐标点，后者是2维空间中的八个坐标点。emmm。。。这么区分除了搞晕脑子之外似乎没什么用处。
    其实，我们可以把后者看成是前者所代表向量的8个分量，两者的表示是等价的。下面我们用短一点的例子来说明。
    <br /><br />假如我们有2对2维空间中的向量
    <br /><blockquote><b>v1</b> = (1, 1), &nbsp;&nbsp; <b>v2</b> = (2, 2)
            <br /><b>w1</b> = (3, 3), &nbsp;&nbsp; <b>w2</b> = (4, 4)
            </blockquote>
    <br /><b>v1</b>和<b>w1</b>终点距离的平方与<b>v2</b>和<b>w2</b>终点距离的平方分别是:
    <br /><blockquote>d1**2 = (3-1)**2 + (3-1)**2，&nbsp;&nbsp; d2**2 = (4-2)**2 + (4-2)**2</blockquote>
    <br />再来看2个4维空间中的向量
    <br /><blockquote><b>v</b> = (1, 1, 2, 2)
            <br /><b>w</b> = (3, 3, 4, 4)
            </blockquote>
    <br />他们之间的距离的平方是：
    <br /><blockquote>d**2 = (3-1)**2 + (3-1)**2 + (4-2)**2 + (4-2)**2</blockquote>
    <br />恰好等于d1**2 + d2**2。
    <br />
    <br />最后补上一张图，描述的是2维向量的两个1维分量（三维空间里解释这个问题也只能用这张图了）的情况，
    同理，对有8个2维分量的16维向量也同样适用：
    <div style="width:420px">
        <img src="vector.png"  width="400px"/>
        <div style="width:300px; margin:0 auto; font-size:10px">
            每次这样随便放张图，高端的感觉立马就来了
        </div>
    </div>
    <br />写到这里，我的思路逐渐理清了，但是不知道有没有把你给绕进去。总之一句话，
    [0, 89, 27,100, 42, 75, 29, 45, 15, 15, 37,  0, 69,  2,100,  6]
    和[(0, 89), (27,100), (42, 75), (29, 45), (15, 15), (37,  0), (69,  2),(100,  6)]两种书写方法是等价的。
    只是前者表示一个16维的向量，而后者表示前者的8个分量。显然，前者的写法用计算机处理起来更加方便。北理工MOOC中将一张
    32*32的图片展开成1*1024的数组，也是同理。
</div>


<div>
    <br />
    OK,写到这里，基本的原理都差不多讲完了，没有严谨的推理，都是些个人理解。下面就结合代码解释一下我的实现。
</div>


<br /><br /><hr />
<h2>动手</h2>
<h4>一点小提示</h4>
<div>
    在Python代码中经常会看到下面这条语句: "if __name__ == '__main__'"
    <br /><img src="if-name.png" width="400px"><br />
    <br />这是Python语言通常用来测试代码的习惯。在Python文件的命名空间中有一个__name__变量，当这个Python文件作为主程序运行的
    时候，__name__的值是'__main__'，当这个Python文件作为模块被其他文件导入的时候，__name__的值就变成了该文件的文件名。当一个
    Python文件被另一个程序导入时会先运行被导入文件中的代码。在编写子模块的过程中可能会想要写些测试代码调试函数的功能，
    但是在实际使用的时候，又不希望这些测试代码运行。这个时候就可以把测试代码放到上述if语句后面，这样只有在该Python文件
    作为主程序运行时才会到达该if语句后的代码，运行测试，而作为模块被导入时，则会跳过测试代码，省去了注释或者删除
    代码的麻烦。
</div>

<br />
<h4>采用DBRHD数据集的第一次实现</h4>
<div>
    老美在数据集的介绍中已经给出了用KNN算法识别的实验结论，正确率都在97%以上，所以我也继续采用KNN算法。代码
    在knn_recog2.py中，与knn_recog.py方法一致，都是直接采用北理工MOOC手写识别实例中的代码，调用scikit-learn
    的KNN 分类器比对数据。不同地方在于数据集不同，所以导入的代码做了相关改动。使用测试集测试的结果如下，符合预期：
    <br /><img src="DBRHD-test-1.png" width="200px"/><br />
    <br />
    做完之后，我自己手写了几个数字做了下实际测试，和下文将要提到的结果一起放在下图：
    <br /><img src="test-result.png"><br />
    <br />
    较之于北理工数据集，识别正确率大幅提高，几乎百发百中了。但是0和1的识别率却几乎是0，让人很沮丧。KNN算法
    背后的欧式距离是严格的数学，识别结果都是严格计算出来的，也就是说，字迹和数据集中的记录越“像”，识别率就
    越高。只要数据集不变，如果故意写得很潦草，那就一定识别不对，如果刻意写得工整，那就一定能识别出来。所以，
    用户多试几次，找到了能让机器认对的写法的话，以后一直这样写，那识别率肯定是100%。但是目前无论我怎样写，0和
    1也就偶尔认对一两次。这说明，我的书写肯定和数据集中的数据很“不像”。可是数据集全是数字，我这个碳基生物还
    真是无法理解。于是，我决定把数据集中的数据给画出来，看看是什么情况。
</div>


<h4>写个脚本画幅画</h4>
<div>
    脚本有两个，先是简单写了个visual_dbrhd.py，手动复制几行数据到代码里，用pygame连线看一看是什么样子。最开始
    我就是用这个脚本结合pendigits.names才终于一点一点摸清数据集的格式。但是这个每次只能画一行数据，想要看到所有的数据的图像
    就无能为力了，所以花了一个下午和一个晚上，认真写了个visualize_paint_dbrhd.py，读取pendigits.tra，把里面的数据
    全部画了出来。画出来的图片在dbrhd_images中。从前面的介绍可知，DBRHD数据集中的一个字迹坐标的取值在0到100之
    间，所以一开始我在一张大图片中分了很多个100*100像素的小框，一个框里画一个字迹，即“100x100满格版”文件夹
    中的图片。其中，0的轨迹图是下面这样子的：
    <br /><img src="visual-DBRHD-1.png" width="500px"> <br />
    <br />白点是起点，红点是其余的点，按顺序用直线连接起来。可以看到所有的0都是逆时针写的，所有！！这就是为什么
    我写的0识别率为0的原因了。返回去试下逆时针写0，识别率99%（说100%不太合适，比如后面将要提到的，可能识别为6）。老外写字的习惯真
    奇葩。不过这样的图中，每个字迹都挤到一起了，好处是容易看出来数据集作者缩放的效果，可以看出来我重采样的方法和老外是一致
    的（证实确实是缩放到填满100*100的空间。这个时候我才确认了这一点，前面介绍的时候，其实是猜测），但是缺点
    也很难受，就是看着不舒服，还是不适合碳基生物。所以我又给每个框加了20像素的内边距，边框变成140x140像素大小。
    1的图片看起来就像是这个样子：
    <br /><img src="visual-DBRHD-2.png" width="500px" /><br />
    <br />额。。。 老美写字都是这么奔放么？也就白圈里这样写感觉比较正常，其他的，都是折线呀，哪里像1了？还是说我不够
    international，对大洋彼岸这种文艺的写法竟然一无所知，实在是naive。所以，识别错误其实是我的错误，怪我
    写得不对。
</div>


<h4>扩展数据集</h4>
<div>But，在一个社会主义共和国，作为一个新时代的<u>优秀</u>青年，还真没见过几个这样写1的。况且，要求用户只能
    逆时针写0，这实在是太不方便了，尤其是对我这样习惯顺时针写的用户来说。所以我们要发挥主观能动性，对数据集进行社会
    主义改造。执行改造的代码是dataSet/dbrhd/目录下的extend_dbrhd.py。做法是：
    <ol>
        <li>将训练集中所有0的数据筛选出来，让所有坐标以直线y=50为轴做轴对称。最后将做了变换后的数据另外保存在
        文件extend_0.tra文件中。</li>
        <li>将训练集中所有1的数据筛选出来，丢弃前两个点，对剩余点缩放之后重采样。最后将做了变换后的数据另外保存
        在文件extend_1.tra中。</li>
    </ol>
    在源码的注释中有更具体的说明。把扩展之后的数据进行可视化，嗯，看到训练集中1的轨迹都变成虫子一样的竖线我就放心了，
    这样对我手写的数字1的识别率应该会有一个很大的提高。但是同时也看到，有些1的轨迹还带个小尾巴，这就可能导致一些手写的2
    被误判成1，事实上，手写3变得很容易被识别成1。这里简单起见，不再继续深入考虑这个问题，只要对1的识别率有显著提升，
    同时对其他数字的识别率影响不大，满足要求
    就可以了。如果后面还需要进一步减小错误率，再回到这个地方继续深入。
    <br /><img src="visual-DBRHD-extend-1.png" width="800px" /><br/>
    <br /><br />有了扩展之后的训练集，新的0和原训练集中的0合并起来就可以同时识别顺时针和逆时针书写的0了。但是原来训练集中1的
    数据我就不想要了，因为它们更像7，可能会导致识别手写7时被误判为1。因此，main2.py中，我在导入DBRHD训练集之后，将其中
    1的数据全部移除，然后导入我建立的扩展训练集extend_0.tra和extend_1.tra，然后将新旧训练集合并。这里为了容易区分，
    扩展的训练集没有直接添加到原训练集的文件中，而是另外保存为以“extend + 数字”命名的文件，在程序运行时，才分别导入后合并。
    <br /><br />
    运行server.py，写几个字试一试，emm，终于可以识别我写的顺时针0和直线1了。当然，没有十全十美的事情，经过改造的训练集
    更加像我手写的字迹，但是却没有之前那么像测试集里的字迹了，尤其是把原训练集中1的数据移除了，会导致训练集中很多1识别错误。
    所以，如果运行knn_recog2.py加载测试集运行测试，就会看到：
    <br /><img src="DBRHD-test-2.png" width="200px"/><br />
    <br />
    实际识别正确率提高了，但是使用测试集测试的正确率却下降了。世间安得两全法，不负如来不负卿。所以我在测试代码前重新载入
    完整的原训练集。这样运行knn_reog2.py输出的是用原来的测试集测试原来训练集的结果，还是97的正确率，用来应付老师和论文。
    而运行main2.py时，重载训练集的代码被跳过，这时使用改进过的训练集，提高实际的识别率。
    <br /><br />
    照理说，这个时候应该算完成任务，
    可以收工了。However，一次偶然的机会，我突然发现，当我从顶部和底部开始写0的时候，又识别不出来了。
    <br /><br />
    <span style="font-size:24px">&nbsp;&nbsp;&nbsp;&nbsp;&lt;表情包&gt;</span>
    <br /><br />
    要是老师非得从顶部开始写0，结果识别不出来，然后搬出来一通大理论说什么合格的程序应该让机器适应人，而不是人来适应机器，
    该怎么办？ _(:з」∠)_
    <br /><br />
    那就只好自己手写几个数字的样本，扩充原来的数据集了。于是就有了input_dbrhd.py。启动这个程序和server.py，在网页上
    写字上传之后，如果想要加入数据集，输入回车就可以将这次手写的轨迹添加进数据集了，如果不满意，输入任意字母再回车，就会放弃这次输入。
    我输入了2个顶部开始顺时针、2顶部开始逆时针、2个底部开始顺时针、2个底部开始逆时针，的0，保存为input_0.tra。
    <br /><br />
    至此，我们就有三类训练集了，分别是：
        <ol>
            <li>原训练集pendigits.tra;</li>
            <li>以前缀“extend”命名的扩展数据集，如extend_0.tra等;</li>
            <li>以前缀“input”命名的输入数据集，或者叫定制数据集，如input_0.tra等。</li>
        </ol>
    <br />
    然后，就有了上图表格中"delete:1; extend: 0,1; input:0;"的实验结果了，测试条件表示的是原训练集中删除数字1的记录，
    合并数字0、1的扩展训练集。
    <br /><br />
    同时看到，3、7的识别率大幅下降，被误识成1，而对数字6而言，如果尾巴占整个数字高度的一半，就经常被识别成0，偶尔是5；如果
    尾巴占高度的一半以上，就会被稳定识别正确。这时候回过头来，一开始训练集中的0全是逆时针的，可能就是老外已经过滤过一次
    数据的结果吧，因为都是顺时针写的，6可能被识别为顺时针写的0。那数字7是咋回事呢？我门来看下原数据集的图片。
    <br /><img src="visual-DBRHD-7.png" width="500px" /><br />
    <br />这咋还精通中国书法呢，写个数字都用回锋笔法。数据集中大部分7的样本底下都要这么往回勾一下，如果我们识别的时候也这么
    勾一下，可以大幅提高正确率。可能真的是我孤陋寡闻吧，还真没见过几个这么写7的。难道当用户咋写都写不对的时候，我们
    要上前问候一句，“您好，写7的时候，下面要这样勾一下哦”。这时候我只想放这张图。
    <br />&nbsp;&nbsp;&nbsp;&nbsp;<img src="hard.jpg" width="300px"><br />
</div>


<h4>总是这样艰辛的</h4>
<div>
    我们可以如法炮制，将原训练集中的数据处理一下，制作一个扩展数据集。对数字7的处理是，去掉最后两个点，然后缩放、
    居中、重采样，一番调教之后，扩展数据集出来了，虽然样子看起来还是不尽如人意，但是也很像我们习惯的写法了。
    <br /><img src="visual-extend-7.png" width="600px"/> <br />
    <br /><br/>
    对于3，可能是我写的3高度远大于宽度，像面条一样，所以常被误判为1。查看原数据集图片里的3，基本上下左右都是
    顶格的。
    <br /><img src="visual-DBRHD-3.png" width="500px"/><br/>
    <br /><br/>
    因为上面的图片填充了内边距，所以看起来不那么明显，下面这张"100x100满格版"的图就能说明问题了。DBRHD中3的轨迹，
    上下左右都是顶格的，而我写的“面条3”，重采样之后跟数据比起来，还是更像1。
    <br /><img src="visual-DBRHD-33.png" width="600px"/><br/>
    <br /><br/>于是我把DBRHD中3的宽度缩小至原来的1/2，然后居中。
    <br /><img src="visual-extend-3.png" width="600px"/><br/>
    <br /><br/>即使缩放到这种程度，连我人眼都觉得是1了，3的识别率也只是提高一点。没办法，机器还是太笨，我教不会，
    只好暂时让聪明的人迁就一下，去适应机器，每次写3的时候，尽量写宽一点。
    如果要优化的话，我能想的到办法是增加重采样点，把目前的8个点提高到10个甚至12个，这样
    采样点对原轨迹的还原度就更高了，即更像原轨迹。但是这样搞工作量就有点大了，要把老外给的原始数据拿出来解析，前面
    的代码也要有不少改动。既然目前效果还不错，就不折腾了。
</div>


<h4>最后的最后 - trajectory-preprocess.py</h4>
<div>
    该说的都差不多说完了，没说的都是不重要的或者用不着的东西。就剩笔迹数据的预处理没说了。在代码的注释中已经有解释了，
    这部分的代码大量使用了numpy，如果要看懂的话，可能要先看下教程。
    这里再简单说明一下处理笔迹输入的步骤。网页采集鼠标或者手指在方框内的坐标，实时添加到一个数组中，同时将各点顺次用
    直线连接，形成轨迹图形。网页上可以看到画出来的是曲线，可见采样率非常高（两点之间距离非常短），传回的数据可能有上百
    个坐标点。用户点击上传按钮后，网页将坐标点组织成文本字符串，将图片以png格式用base64编码转换成文本，以json格式发送
    给服务器。服务器将坐标点保存为sample.txt， 将图片base64解码后保存为sample.png。然后main2.py检测到sample.txt有变化，
    就读取文件内容，调用trajectory_preprocess.py中的函数对轨迹进行处理，其中包括resample、scale、center、
    coordination_translate4个操作，目的是将数据处理成能和训练集比较的形式，并且尽可能和训练集里的轨迹相似（顶格、居中）。
    iMOOC里还提到了rotate，因为比较麻烦，但是感觉用处不大，所以就没做。
    <br/><br/>
    主要是resample的算法比较难理解，虽然是我写的，但是其实我也不是完全理解0.0 。甚至还有bug（用try-except圈起来了，
    程序不会崩溃，但是会没有结果，重试一次就好了）。
    这里大致讲解下思路。首先把所有点顺次连接起来，首尾不闭合，计算总长度，处以7（8-1=7，resample后的线段数量）得到平均
    长度，然后根据这个平均长度将轨迹等分成7份，这7个线段的8个端点就是重采样点了。难点是怎么计算出来重采样点的坐标。我的
    方法是，从起点开始，将两点之间的距离累加起来，和平均长度比较，假如小于平均长度，就累加与下一个点之间的距离，一直加
    这样的两点，加上这两点的距离，则总距离大于平均长度，缺少这段距离，则总距离小于平均长度，说明重采样点就在这两点之间。
    然后看看再加上多少长度就够平均长度了，用这个尚还缺少的长度和这两点之间的距离就可以根据比例求出重采样点的坐标，这个
    缺少的长度，在程序中我命名为diff。空谈误人，上图易懂，下面这张图解释了我的想法。估计这段代码应该没人会碰了，因为
    看代码会很难受，而且能跑起来的话，也没必要动。假如真的要动这段代码，可能还是自己重写会比较舒服吧。
    代码看起来可能比较费劲，我也说不清楚，只好放个图弥补一下。
    <br/><img src="resample算法图解.png" width="800px"/><br/>
    <br/><br/>
</div>


<hr/>
<h2>End</h2>
<div>
    恭喜你看到了这里，你已经知道了我知道的所有东西，我再没有什么能写的了。写完之后，我感觉到自己实在词穷，脑子里
    的想法就像茶壶里的饺子，倒不出来，许多表述也不太准确。比如文章中扩展数据集什么的操作指的
    都是对训练集的操作。但是写得顺了，脑子没怎么思考，张口就莱，总是数据集数据集的就写下去了，实际上大多时候指的
    是训练集。但是训练集和测试集格式都是一样的，只是用途不一样，对训练集能做的操作，对测试集也能做。
    <br /><br/>
    同样的还有向量，当谈到距离的时候，我会强调是“两向量终点的距离”。其实这样表述有点啰嗦，因为实际上我们讨论的
    是空间中点的距离，而空间中一点与坐标系原点很自然就构成了向量，而构成向量的目的就是利用向量的性质，可以方便很
    多计算。所以说“两向量之间的距离”的时候，实际指的就是两终点的距离，并不会引起歧义，而表达起来更简洁些。

    所以阅读中感到迷惑的时候不必太死抠字眼，因为可能我也没想清楚。
    <br/><br />
    希望对你有所帮助。
</div>


<br /><br /><hr>
<b>2019-2-20增加</b>
<h2>最后简单说下代码的使用</h2>
<div>
    因为要凑行数，所以一些操作我尽量展开写了，有些本该写成函数或者写成模块以供其他模块调用的，这样会比较清晰合理。但是
    为了写长点，在不影响开发的情况下都展开写了，所以会有些重复的代码，有些一行可以搞定的语句，我也分步写成多行了。比如
    server.py和server2.py的代码除了两三行之外，其余都一样。这样的坏处是日后要改服务器的话，要改两份文件，不过好处是，两倍长
    度的代码，况且server.py因为识别率低应该不怎么用了。

    下面大体介绍一下代码的体系，以便用户有一定的了解。
</div>


<h4>大致分类</h4>
<div>
    整个项目的代码确实有点乱，不过总体上可以将代码分为这么三类：
    <ol>
        <li>连接用户输入的server程序，和实现识别的程序，即knn_recog、main等程序</li>
        <li>对数据进行处理的辅助模块，即各种preprocess程序</li>
        <li>扩展数据集或者可视化数据的工具程序，即各种extend和visual类程序</li>
    </ol>
</div>


<h4>数字识别功能</h4>
<div>
    数字识别的功能分两步。第一步是由preprocess程序将用户输入（图片或字迹）预处理成与数据集统一的格式以便用于
    识别。第二步是在knn_recog程序中，借助scikit-learn库，导入数据集和训练分类器，提供识别的接口。main程序负责
    协调执行这两个步骤，将第一步预处理得到特征向量输入第二步中的程序，获得结果，然后在控制台（cmd、终端）print结果。
    至此已经实现数字识别功能。在这两步之外还有一个并行的工作，即通过server和网页（draw.html）的结合获取用户输入，并生成文件
    保存在工作目录下。main检测到文件变动就读取文件执行上述两步进行识别。
    <br /><br />
    因为获取输入已经用了网页，所以识别结果的显示用网页更方便。html+css+js技术更容易写出好看的界面，还同时兼容手机和电脑。网络
    数据的传输使用了javascript + ajax技术，避免了更新数据后需要刷新网页。由于识别过程的输入和输出都用到server，所以这时
    可以由server替代main的功能，单独的main已经没有存在的必要了。实际的实现是，在server中将main作为模块导入，从而使用main的功能，
    相当于把main又封装一层。
    <br /><br />
    用户的输入有两种，一种是图片，另一种是用户在网页上写字时，实时记录下鼠标的位置，得到手写轨迹的一系列二维坐标点。正如
    前面提到的，两种方案我都试了，识别的算法都用KNN。第一种直接识别图片，这种方法依赖于img_preprocess.py
    的辅助，将图片处理成32x32像素大小，然后输入knn_recog.py进行识别。服务器是server.py。由于实际识别率低（但是使用
    测试集测试也有95以上的正确率），所以这个方案被我放弃，但是这组程序仍然是可以运行的。第二种方案是识别输入的轨迹，
    过程和第一种一样，程序名称都多了一个2，命名为server2.py，main2.py和knn_recog.py。不同之处是预处理的程序变为
    trajectory_preprocess.py。
    <br />
</div>


<h4>扩展数据集</h4>
<div>
    前面通篇都在讲拓展数据集的方法，这里简要介绍一下程序部分。由于是对dbrhd数据集进行操作，所以这类代码的文件名中都会有
    单词“dbrhd”。extend训练集的程序名为extend_dbrhd.py，位置为dataSet/dbrhd/extend_dbrhd.py，和训练集放在一起。扩展
    数据集是针对特定数字分别进行的，操作都是特别的，这些操作分别写成了函数。input训练集的程序名为input_dbrhd.py，
    用于将用户的输入转化成数据集的格式，进而生成数据集。
</div>


<h4>可视化数据集</h4>
<div>
    这也是针对dbrhd数据集而言的。北理工数据集已经是图片，打开文件即可直接看到字迹的形状。用于可视化的程序有两个，一个是
    visual_dbrhd.py，一个是visualize_pain_dbrhd.py。visualize_pain_dbrhd.py是独立运行的脚本，用于将一个数据集中的所有数据绘制在一张大图片
    中，保存输出图片的文件夹被设置为dbrhd_images。使用时需要设置数据集的文件名和输出图片文件名的前缀。首先在程序开头找到
    PATH和PREFIX变量，PATH为将要绘制的数据集，PREFIX为输出图片文件名的前缀。输出图片的全部文件名为“前缀+数字_编号.png”。
    如下图设置就是将extend_3.tra数据集中的所有数据画在一张大图上，图片文件名为“extend3_编号.png”，例如“extend3_1.png”。
    <br /><img src="visualize_paint_dbrhd.png" width="700px"/><br/>
    文件名中的数字取自数据集中的记录（每行的最后一个数字）。编号从1开始递增，如果程序发现该目录下已有文件名为
    “extend3_1.png”,则编号递增1，将图片命名为“extend3_2.png”并保存。
    <br />
    visual_dbrhd.py则是被其他程序调用的子模块，提供函数接口，生成单张图片。server程序调用这个模块用来生成发送给网页的图片。
</div>


<h4>已知的bug</h4>
<div>
    目前有时会出现一个已知的bug，就是当在网页中绘制极快，或者轨迹极短时，网页可能没有采集到足够的采样点，因此重采样就会出错。
    这个bug不会引起程序崩溃，而且由于网页的采样率很高，正常书写数字几乎不会出现这个bug，只有在比较极端的情况下，比如写了一个
    “点”，就提交服务器识别。
</div>

<h4>没有了</h4>

</body>
</html>